﻿namespace EasyToolkit.ModelDefinition
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using EasyToolkit.Tools.Extensions;
    using EasyToolkit.ModelDefinition.Definition.XmlDefinition;
    using EasyToolkit.Tools.Reflection;

    class ModelType : IModelType
    {
        #region properties and attributes
        private readonly object syncObj = new object();
        private string reference;
        private bool dependenciesResolved;
        private Action retrieveProperties;

        public Dictionary<string, IModelProperty> PropertiesByName
        { get; private set; }

        public string Name
        { get; private set; }

        public string Description
        { get; private set; }
        #endregion

        #region .ctors
        public ModelType(XmlModelTypeDefinition definition)
        {
            if (definition != null)
            {
                try
                {
                    if (string.IsNullOrEmpty(definition.Name))
                        throw new EasyToolkitException("'Name' is mandatory");

                    PropertiesByName = new Dictionary<string, IModelProperty>();
                    retrieveProperties = new Action(() => RetrievePropertiesFromXml(definition.Properties));

                    Name = definition.Name.EmptyIfNull().Trim();
                    Description = definition.Description;
                    reference = definition.Reference.EmptyIfNull().Trim();
                    string typeName = definition.Type.EmptyIfNull().Trim();
                    string assemblyName = definition.Assembly.EmptyIfNull().Trim();

                    if (string.IsNullOrEmpty(reference) && string.IsNullOrEmpty(typeName) 
                        && (definition.Properties == null || definition.Properties.Count() == 0))
                        throw new EasyToolkitException("No 'Reference', 'Type' or 'Properties' defined");

                    if (!string.IsNullOrEmpty(reference) && !string.IsNullOrEmpty(typeName))
                        throw new EasyToolkitException("'Referefence' and 'Type' cannot be both defined");

                    // reference are resolved at the first use.
                    if (string.IsNullOrEmpty(reference))
                    {
                        if (!string.IsNullOrEmpty(typeName) && !string.IsNullOrEmpty(assemblyName))
                            RetrievePropertiesFromType(assemblyName, typeName);
                    }
                }
                catch (Exception ex)
                {
                    throw new EasyToolkitException(string.Format("Cannot create 'ModelDataType' '{0}': {1}", definition.Name.EmptyIfNull(), ex.Message));
                }
            }
        }
        #endregion

        #region public methods 
        public IEnumerable<IModelProperty> GetProperties()
        {
            if (! dependenciesResolved)
                ResolveDependencies();

            IEnumerable<IModelProperty> ret = null;
            if (PropertiesByName != null)
                ret = PropertiesByName.Values.OrderBy(p => p.Name).ToList();
            return ret;
        }

        public IModelProperty GetProperty(string name)
        {
            if (!dependenciesResolved)
                ResolveDependencies();

            IModelProperty ret = null;
            if (PropertiesByName != null)
                PropertiesByName.TryGetValue(name, out ret);
            return ret;
        }

        public void ResolveDependencies()
        {
            lock (syncObj)
            {
                if (! dependenciesResolved)
                {
                    if (!string.IsNullOrEmpty(reference))
                        RetrievePropertiesFromReference();

                    if (retrieveProperties != null)
                        retrieveProperties();
                    dependenciesResolved = true;
                }            
            }
        }
        #endregion

        #region private methods
        private void RetrievePropertiesFromType(string assemblyName, string typeName)
        {
            Type type = TypeHelpers.GetType(assemblyName, typeName);
            if (type == null)
                throw new EasyToolkitException(string.Format("Cannot find the type '{0}.{1}'", assemblyName, typeName));

            IEnumerable<IModelProperty> boundProperties = ModelBoundProperty.CreateInstance(type);
            if (boundProperties != null)
            {
                foreach (IModelProperty p in boundProperties)
                    PropertiesByName[p.Name] = p;
            }
        }

        /// <summary> If Xml property is defined, it will override the existing property having the same name.</summary>
        private void RetrievePropertiesFromXml(List<XmlModelPropertyDefinition> properties)
        {
            if (properties != null)
            {
                try
                {
                    foreach (XmlModelPropertyDefinition property in properties)
                    {
                        IModelProperty modelProperty; 
                        if (string.IsNullOrEmpty(property.Name))
                            throw new EasyToolkitException("A property name is null or empty");
                        if (!string.IsNullOrEmpty(property.TypeDefinition))
                            modelProperty = ModelLinkProperty.CreateInstanceFromXmlDefinition(this, property);
                        else
                            modelProperty = new ModelProperty(property.Name, property.Description);

                        PropertiesByName[property.Name] = modelProperty;
                    }
                }
                catch (Exception ex)
                {
                    throw new EasyToolkitException(string.Format("Cannot retrieve properties for type '{0}': {1}", Name.EmptyIfNull(), ex.Message));
                }
            }
        }

        private void RetrievePropertiesFromReference()
        {
            try
            {
                IModelType typeReference = ModelDefinitionManager.Instance.GetModelType(reference);
                if (typeReference == null)
                    throw new EasyToolkitException(string.Format("Cannot retrieve the referenced model type '{0}'", reference));

                (typeReference as ModelType).ResolveDependencies();

                foreach (IModelProperty property in typeReference.GetProperties())
                    PropertiesByName[property.Name] = property;
            }
            catch (Exception ex)
            {
                throw new EasyToolkitException(string.Format("Model type '{0}': Cannot resolve the model type reference:{1}", Name.EmptyIfNull(), ex.Message), ex);
            }
        }
        #endregion
    }
}
